#pragma once

#include <istream>
#include <iosfwd>
#include <cwctype>

namespace CppSaxXmlParser
{

class Parser;

class IXmlListener
{
public:
	virtual void onHeaderOpen(const Parser& parser){};
	virtual void onHeaderClose(const Parser& parser){};
	virtual void onTagOpen(const Parser& parser, const std::wstring& tagName){};
	virtual void onTagClose(const Parser& parser, const std::wstring& tagName){};
	virtual void onTagText(const Parser& parser, const std::wstring& tagText, const std::wstring& tagName){};
	virtual void onAttribute(const Parser& parser, const std::wstring& attributeName, const std::wstring& attributeValue){};
	virtual void onCDATA(const Parser& parser, const std::wstring& cdata){};

protected:
	virtual ~IXmlListener(){};
};

class EofException : public std::exception
{
};

class Parser
{
public:
	typedef std::wistream stream_type;
	typedef stream_type::char_type char_type;
	typedef std::istream_iterator<char_type, char_type> iterator_type;

	Parser(IXmlListener& listener, stream_type& inputstream)
		: m_listener(listener)
		, m_stream(inputstream)
		, m_it(m_stream)
	{
		m_stream.unsetf(std::ios::skipws);
	}

	void parse() throw(...)
	{
		try
		{
			advanceToFirstTag();
			parseTags();
		}
		catch (EofException&)
		{
			return;
		}
	}

private:
	void advanceToFirstTag()
	{
		skipWhiteSpace();
		if (!isCurrentIsLessSign())
		{
			throw std::exception("Document is empty");
		}

		advance();
	}

	void parseTags()
	{
		if (isCurrentIsQuestionSign())
		{
			advance();
			captureName();
			m_listener.onHeaderOpen(*this);
			parseAttributes();

			if (isCurrentIsQuestionSign())
			{
				advance();
				if (isCurrentIsGreaterSign())
				{
					m_listener.onHeaderClose(*this);
					advance();
					advanceToFirstTag();
					parseTags();
					return;
				}
			}

			throw std::exception("Malformed header");
		}
		else if (isCurrentIsExclamationSign())
		{
			parseCommentOrDoctype();
			return;
		}

		const std::wstring tagName = captureName();
		m_listener.onTagOpen(*this, tagName);
		
		parseAttributes();

		if (isCurrentIsSlashSign())
		{
			advance();
			m_listener.onTagClose(*this, tagName);
			checkCurrentIsGreaterSign();
			return;
		}

		checkCurrentIsGreaterSign();

		while (true)
		{
			const std::wstring tagText = captureText();

			if (!tagText.empty())
			{
				m_listener.onTagText(*this, tagText, tagName);
			}

			if (!isCurrentIsLessSign())
			{
				throw std::exception("Invalid tag open");
			}

			advance();
			if (isCurrentIsSlashSign())
			{
				advance();
				const std::wstring tagCloseName = captureName();

				if (tagName != tagCloseName)
				{
					throw std::exception("Close tag name mismatch");
				}

				skipWhiteSpace();
				m_listener.onTagClose(*this, tagName);

				checkCurrentIsGreaterSign();
				return;
			}
			
			parseTags();
		}
	}

	void parseCommentOrDoctype()
	{
		advance();

		if (currentChar() == L'-')
		{
			parseComment();
		}
		else if (currentChar() == L'[')
		{
			parseCDATA();
		}
		else if (currentChar() == L'D')
		{
			parseDoctype();
		}
	}

	void parseComment()
	{
		advance();
		if (currentChar() != L'-')
		{
			throw std::exception("invalid comment");
		}

		advance();

		std::wstring commentString;

		while (commentString.find(L"-->") == std::wstring::npos)
		{
			commentString += currentChar();
			advance();
		}
	}

	void parseCDATA()
	{
		const std::wstring cdataBegin(L"[CDATA[");

		std::wstring capturedBegin;
		for (size_t i = 0; i < cdataBegin.size(); ++i)
		{
			capturedBegin += currentChar();
			advance();
		}

		if (capturedBegin != cdataBegin)
		{
			throw std::exception("invalid CDATA");
		}

		const std::wstring cdataEnd(L"]]>");
		std::wstring cdataString;

		while (cdataString.find(cdataEnd) == std::wstring::npos)
		{
			cdataString += currentChar();
			advance();
		}

		m_listener.onCDATA(*this, cdataString.substr(0, cdataString.size() - cdataEnd.size()));
	}

	void parseDoctype()
	{
		const std::wstring doctype = captureName();

		if (doctype != L"DOCTYPE")
		{
			throw std::exception("Invalid doctype name");
		}
	}

	void parseAttributes()
	{
		skipWhiteSpace();
		const std::wstring attributeName = captureName();

		if (attributeName.empty())
		{
			return;
		}

		skipWhiteSpace();

		if (!isCurrentIsEqualSign())
		{
			throw std::exception("Specification mandate value for attribute");
		}

		advance();

		skipWhiteSpace();
		checkCurrentIsQuotationSign();

		const std::wstring attributeValue = captureAttributeValue();

		checkCurrentIsQuotationSign();

		m_listener.onAttribute(*this, attributeName, attributeValue);

		parseAttributes();
		return;
	}

	void skipWhiteSpace()
	{
		while (isCurrentIsSpace())
		{
			advance();
		}
	}

	bool isCurrentEqualsSpecialChar()
	{
		return isCurrentIsLessSign() || isCurrentIsGreaterSign() || isCurrentIsQuestionSign()
			|| isCurrentIsQuotationSign() || isCurrentIsEqualSign() || isCurrentIsSlashSign();
	}

	const std::wstring captureText()
	{
		std::wstring result;
		while (!isCurrentIsLessSign())
		{
			result += currentChar();
			advance();
		}

		return result;
	}

	const std::wstring captureAttributeValue()
	{
		std::wstring result;
		while (!isCurrentIsQuotationSign())
		{
			result += currentChar();
			advance();
		}

		return result;
	}

	const std::wstring captureName()
	{
		std::wstring result;
		while (!isCurrentEqualsSpecialChar() && !isCurrentIsSpace())
		{
			result += currentChar();
			advance();
		}

		return result;
	}

	char_type currentChar() const
	{
		return *m_it;
	}

	bool isCurrentIsQuestionSign() const
	{
		return currentChar() == L'?';
	}

	bool isCurrentIsLessSign() const
	{
		return currentChar() == L'<';
	}

	void checkCurrentIsGreaterSign()
	{
		if (!isCurrentIsGreaterSign())
		{
			throw std::exception("Invalid tag close");
		}

		advance();
	}

	bool isCurrentIsGreaterSign() const
	{
		return currentChar() == L'>';
	}

	bool isCurrentIsEqualSign() const
	{
		return currentChar() == L'=';
	}

	bool isCurrentIsExclamationSign() const
	{
		return currentChar() == L'!';
	}

	bool isCurrentIsSlashSign() const
	{
		return currentChar() == L'/';
	}

	bool isCurrentIsSpace() const
	{
		return std::iswspace(currentChar()) != 0;
	}

	void checkCurrentIsQuotationSign()
	{
		if (!isCurrentIsQuotationSign())
		{
			throw std::exception("Expected quotation sign");
		}

		advance();
	}

	bool isCurrentIsQuotationSign() const
	{
		return currentChar() == L'\"' || currentChar() == L'\'';
	}

	void advance()
	{
		m_it++;
		
		if (m_it == iterator_type())
		{
			throw EofException();
		}
	}

	IXmlListener& m_listener;
	stream_type& m_stream;
	iterator_type m_it;
};

}